/*
 *  linux/boot/head.s
 *
 *  (C) 1991  Linus Torvalds
 */

/*
 *  head.s contains the 32-bit startup code.
 *
 * NOTE!!! Startup happens at absolute address 0x00000000, which is also where
 * the page directory will exist. The startup code will be overwritten by
 * the page directory.
 */
.text
# 声明head.s中的以下标号为全局符号, 供后续C程序使用。
.globl idt,gdt,pg_dir,tmp_floppy_area
# 页表目录(数据结构)起始处。
pg_dir:
.globl startup_32

# 在当前的Linux 操作系统中，gas 和gld 已经分别更名为as 和ld。
# 再次注意!!! 这里已经处于32 位保护模式，段寄存器中存储的是段描述符


# 段选择符格式：
# |15               3| 2|1     0|
# -------------------------------
# |         索引      |TI|RPL\CPL|
# -------------------------------
# RPL 请求特权级字段，它提供了段保护信息，Linux 操作系统只用到两级：0级（内核级）和3级（用户级）；
# TI 表示包含指定描述符的段描述符表 GDT或LDT。TI=0 表示描述符在 GDT 中；TI=1 表示描述符在LDT 中。
# 索引字段给出了描述符在 GDT 或 LDT 表中的索引项号。
# 选择符通过定位段表中的一个描述符来指定一个段，并且描述符中包含有访问一个段的所有信息，例如段的基地址、段长度和段属性。
#  0b000000000000001000 = 0x08 表示 GDT 表中的索引1项，内核代码段。
#  0b000000000000010000 = 0x10 表示 GDT 表中的索引2项，内核数据段。
#  0b000000000000001111 = 0x0f 表示 LDT 表中的索引1项，任务代码段。
#  0b000000000000010111 = 0x17 表示 LDT 表中的索引2项，任务数据段。
#
# 下面代码的含义是：置ds,es,fs,gs 中的选择符为setup.s 中构造的数据段（全局段描述符表的第2 项）=0x10，
# 并将堆栈放置在数据段中的_stack_start 数组内，然后使用新的中断描述符表和全局段描述表。
# 新的全局段描述表中初始内容与setup.s 中的基本一样。仅段限长从8M改为了16M
startup_32:
    # 每个寄存器名都要以'%'开头，eax 表示是32 位的ax 寄存器。
    # 进入保护模式后, 首先需将段描述符的选择符加载给各段寄存器。
    # 在给各数据段寄存器赋值时, 若合法的话,
    # CPU会将段描述符选择符0x10对应的段描述符的内容隐式加载到
    # 数据段寄存器的隐藏部分。
	movl $0x10,%eax
	mov %ax,%ds
	mov %ax,%es
	mov %ax,%fs
	mov %ax,%gs
	# stack_start 定义在 kernel/sched. c,
	# 设置ss:esp维护的栈内存,
    # 将stack_start内存处的低32位赋给esp, 高16位赋给ss。
    # stack_start高16位值为0x10, 低32位为&user_stack[PAGE_SIZE>>2],
    # ss:esp=0x10:&user_stack[PAGE_SIZE>>2]
    # 表明栈内存在 段描述符选择符0x10对应内存段 中的user_stack数组。
	lss stack_start,%esp

	# 重新设置IDT和GDT到head.s所在内存段中(0x0)。
	call setup_idt
	call setup_gdt

	# 与setup.s中的设置相比,
    # 此处更新了可执行程序段和数据段的长度(由8Mb到16Mb),
    # linux 0.11操作系统程序只读取了192Kb,
    # 操作系统程序之后的内存(到16Mb处)用于外设缓冲区和
    # 内核数据结构的动态分配等。
    # 重新设置GDT后, 数据段描述符更新了内存段的长度,
    # 重新加载数据段描述符到各数据段寄存器中。
	movl $0x10,%eax		# reload all the segment registers
	mov %ax,%ds		# after changing gdt. CS was already
	mov %ax,%es		# reloaded in 'setup_gdt'
	mov %ax,%fs
	mov %ax,%gs

	# 由于段描述符中的段限长从 setup.s 中的8MB 改成了本程序设置的16MB（见 setup.s 行 208-216
    # 和本程序后面的235-236 行），因此这里再次对所有段寄存器执行加载操作是必须的。另外，通过
    # 使用 bochs 跟踪观察，如果不对CS 再次执行加载，那么在执行到 26 行时 CS 代码段不可见部分中
    # 的限长还是8MB。这样看来应该重新加载 CS。但是由于 setup.s中的内核代码段描述符与本程序中
    # 重新设置的代码段描述符除了段限长以外其余部分完全一样，8MB 的限长在内核初始化阶段不会有
    # 问题，而且在以后内核执行过程中段间跳转时会重新加载 CS。因此这里没有加载它并没有让程序
    # 出错。
	lss stack_start,%esp

    # 用于测试 A20 地址线是否已经开启。采用的方法是向内存地址 0x000000 处写入任意
    # 一个数值，然后看内存地址 0x100000（1M） 处是否也是这个数值。如果一直相同的话，就一直
    # 比较下去，也即死循环、死机。表示地址 A20 线没有选通，结果内核就不能使用 1MB 以上内存。
	xorl %eax,%eax
1:	incl %eax		# check that A20 really IS enabled
	movl %eax,0x000000	# loop forever if it isn't
	cmpl %eax,0x100000
	je 1b

/*
 * NOTE! 486 should set bit 16, to check for write-protect in supervisor
 * mode. Then it would be unnecessary with the "verify_area()"-calls.
 * 486 users probably want to set the NE (#5) bit also, so as to use
 * int 16 for math errors.
 */
	movl %cr0,%eax		# check math chip
	andl $0x80000011,%eax	# Save PG,PE,ET
/* "orl $0x10020,%eax" here for 486 might be good */
	orl $2,%eax		# set MP
	movl %eax,%cr0
	call check_x87
	# 最后设置页表目录和页表,
    # 页表目录_pg_dir将覆盖内存[0x0, 0x1000),
    # 4个页表pg0-pg3将覆盖占用内存[0x1000, 0x5000),
    # 1个页表目录和1个页表各占4Kb内存, 它们会覆盖操作系统程序前20Kb内容。
	jmp after_page_tables

/*
 * We depend on ET to be correct. This checks for 287/387.
 */
check_x87:
	fninit
	fstsw %ax
	cmpb $0,%al
	je 1f			/* no coprocessor: have to set bits */
	movl %cr0,%eax
	xorl $6,%eax		/* reset MP, set EM */
	movl %eax,%cr0
	ret
.align 2
1:	.byte 0xDB,0xE4		/* fsetpm for 287, ignored by 387 */
	ret

/*
 *  setup_idt
 *
 *  sets up a idt with 256 entries pointing to
 *  ignore_int, interrupt gates. It then loads
 *  idt. Everything that wants to install itself
 *  in the idt-table may do so themselves. Interrupts
 *  are enabled elsewhere, when we can be relatively
 *  sure everything is ok. This routine will be over-
 *  written by the page tables.
 */
 # 1、在idt的位置设置256个IDT描述符，程序的入口偏移地址是ignore_int
 # 2、将IDT的地址和长度加载到IDTR寄存器。#
 #
 # 中断描述符之中断门的格式
 # |31                       16|15 |14 13|12       8 |           0
 # ---------------------------------------------------------------
 # | ignore_int offset[31..16] | P | DPL | 0 1 1 1 0 | 000 |     |  = edx
 # ---------------------------------------------------------------
 # |          段选择符          |     ignore_int offset[15..0]    |  = eax
 # ---------------------------------------------------------------
 # IDT描述符位含义可参考setup.s。
 # 在后续C程序中, 会根据需求陆续设置相应的IDT描述符。#

setup_idt:
	lea ignore_int,%edx    #lea是“load effective address”的缩写，用于将一个内存地址直接赋给目的操作数，
	movl $0x00080000,%eax  # eax的值将被用于设置到门描述符的低32位。
	                       # 根据门描述符的格式，0x0008 对应的就是段选择符。
                           # 根据段选择符的格式，0x0008 = 0b1 0 00, 表示gdt表中第二个描述符, 内核的代码段
	movw %dx,%ax		   # 将edx的低16位（dx），也就是ignore_int地址的低16位，赋给eax的低16位（ax）所以 eax=0x0008 ignore_int地址的低16位 。
	movw $0x8E00,%dx	   # edx的值将被用于设置到门描述符的高32位。
	                       # edx 的高16位是 ignore_int地址的高16位，edx 的低16位是0x8E00
                           # 0x8e00 = 0b1000 1110 0000 0000 下面依次从左往右分析各位的意思
                           # 最高位1 ：P 表示门描述符有效，
                           # 00: DPL 0级特权级
                           # 0: 门描述符固定的值，
                           # 1110 :Type，1110表示 32位中断门

	lea idt,%edi         # 取idt表地址 存入 edi寄存器
	mov $256,%ecx        # 重复256次, 循环设置所有256个门描述符项。
rp_sidt:
	movl %eax,(%edi)  # 设置门描述符的低4字节，mov eax的内容，到 edi的内存处，这里的括号是寄存器间接寻址
	movl %edx,4(%edi) # 设置门描述符的高4字节， Gun汇编器不允许直接把寄存器和数字相加 ，所以用4(%edi),表示edi的地址 + 4
	addl $8,%edi      # +8, 即下一个门描述符地址，再次循环时，就就可以设置下一门描述符
	dec %ecx          # ecx自减
	jne rp_sidt       #
	lidt idt_descr    # 最后用6字节操作数 加载 LDTR
	ret

/*
 *  setup_gdt
 *
 *  This routines sets up a new gdt and loads it.
 *  Only two entries are currently built, the same
 *  ones that were built in init.s. The routine
 *  is VERY complicated at two whole lines, so this
 *  rather long comment is certainly needed :-).
 *  This routine will beoverwritten by the page tables.
 */
setup_gdt:
	lgdt gdt_descr  # 加载GDT的长度和基址到GDTR寄存器中。GDT表已经设置好，在标号gdt处
	ret

/*
 * I put the kernel page tables right after the page directory,
 * using 4 of them to span 16 Mb of physical memory. People with
 * more than 16MB will have to expand this.
 */

# head.s将内核页表数据结构紧跟在页表目录_pg_dir之后, #
# 此处预留4个页表,
# 每个页表0x1000 = 4k，也就是1K个页表项，1个页表项映射4K的物理内存，一个页表映射4M的物理内存
# 4个页表将映射[0x0, 0x1000000)16Mb物理内存。#
.org 0x1000  #伪指令,地址后续程序的起始地址
pg0:

.org 0x2000
pg1:

.org 0x3000
pg2:

.org 0x4000
pg3:

.org 0x5000
/*
 * tmp_floppy_area is used by the floppy-driver when DMA cannot
 * reach to a buffer-block. It needs to be aligned, so that it isn't
 * on a 64kB border.
 */
 # tm_floppy_area这1Kb内存用于当DMA获取不到buffer时供软驱使用的场景。#
 # 由于需要内存对齐, 该段内存并没有横跨以64Kb对齐的内存边界。#
tmp_floppy_area:
	.fill 1024,1,0


#下面这几个入栈操作用于为跳转到 init/main. c 中的 main(） 函数作准备工作。
# 当 head. s 最后在第 365 行执行 ret 指令时就会弹出 main()的地址，
# 并把控制权转移到 init/main.c程序中。参见第3章中有关C函数调用机制的说明。


after_page_tables:
    # 3个入栈0值应该分别表示 envp、 argy 指针和 argc 的值，但 main(） 没有用到。
	pushl $0		# These are the parameters to main :-)
	pushl $0
	pushl $0
	pushl $L6		# return address for main, if it decides to.
	                # 模拟调用 main.c 程序时首先将返回地址入栈的操作，
	                # 如果 # main.c 程序真的退出时，就会返回到这里的标号L6处继续执行下去，也即死循环。
	pushl $main     # 压入了 main(） 函数代码的地址。
	jmp setup_paging
L6:
	jmp L6			# main should never return here, but
				# just in case, we know what happens.

/* This is the default interrupt "handler" :-) */
int_msg:
	.asciz "Unknown interrupt\n\r"
.align 2
# ignore_init子程序
# 调用printk(定义在kernel/prink.c)打印
# 用于提示未知中断("Unknown interrupt\n\r")的字符串。
# ignore_init函数被本程序用于以中断门的格式初始化IDT。
ignore_int:
	pushl %eax
	pushl %ecx
	pushl %edx
	push %ds         # 这里请注意！！ds, es,fs,gs 等虽然是16 位的寄存器，但入栈后
                     #仍然会以32 位的形式入栈，也即需要占用4个字节的堆栈空间。
	push %es
	push %fs
	movl $0x10,%eax
	mov %ax,%ds
	mov %ax,%es
	mov %ax,%fs
	pushl $int_msg   #把调用 printk 函数的参数指针（地址）入栈。注意！若符号 int_msg
                     # 前不加”$’，则表示把 int._msg 符号处的长字（”Unkn’）入栈◎。

	call printk     # 该函数在/kernel/printk.c中。
	popl %eax
	pop %fs
	pop %es
	pop %ds
	popl %edx
	popl %ecx
	popl %eax
	iret


/*
 * Setup_paging
 *
 * This routine sets up paging by setting the page bit
 * in cr0. The page tables are set up, identity-mapping
 * the first 16MB. The pager assumes that no illegal
 * addresses are produced (ie >4Mb on a 4Mb machine).
 *
 * NOTE! Although all physical memory should be identity
 * mapped by this routine, only the kernel page functions
 * use the >1Mb addresses directly. All "normal" functions
 * use just the lower 1Mb, or the local data space, which
 * will be mapped to some other place - mm keeps track of
 * that.
 *
 * For those with more memory than 16 Mb - tough luck. I've
 * not got it, why should you :-) The source is here. Change
 * it. (Seriously - it shouldn't be too difficult. Mostly
 * change some constants etc. I left it at 16Mb, as my machine
 * even cannot be extended past that (ok, but it was cheap :-)
 * I've tried to show which constants to change by having
 * some kind of marker at them (search for "16Mb"), but I
 * won't guarantee that's all :-( )
 */
 # 子程序通过设置CR0寄存器PG位开启页机制, #
 # 并设置4个页表对应物理内存的 0b-16Mb 的内存。#
 #
 # 注：尽管所有的物理内存都可以通过页表目录、页表数据结构映射, #
 # 但只有操作系统内核中的页操作函数才会直接使用扩展内存(1M以外的)。#
 # 其余函数所使用的内存将会由内存管理模块mm内的函数管理分配。#

 # 上面英文注释第2段的含义是指在机器物理内存中大于 1MB 的内存空间主要被用于主内存区。
 # 主内存区空间由 mm 模块管理。它涉及到页面映射操作。内核中所有其他函数就是这里指的一般
 #（普通）函数。若要使用主内存区的页面，就需要使用 get_free_ page () 等函数获取。
 # 因为主内存区中内存页面是共享资源，必须有程序进行统一管理以避免资源争用和竞争。
 # 在内存物理地址 0x0 处开始存放1页页目录表和4页页表。
 # 页目录表是系统所有进程公用的，而这里的4页页表则属于内核专用，它们一一映射线性地址起始 16MB 空间范围到物理内存上。
 # 对于新的进程，系统会在主内存区为其申请页面存放页表。
.align 2
# 设置页表目录和页表, 开启页机制。
setup_paging:
    # 以下这段汇编指令将[0x0, 0x5000)这20k内存初始化为0。也就是pg_dir、pg0、pg1、pg2、pg3这5个区间，各4K
	movl $1024*5,%ecx		/* 5 pages - pg_dir+4 page tables */
	xorl %eax,%eax
	xorl %edi,%edi			/* pg_dir is at 0x000 */

	# cld使DF（Direction Flag）复位，DF=0，使得si或di向高地址增加
	# std使DF置位，DF=1，使得si或di向低地址减小

    #rep可以是任何字符传指令(CMPS, LODS, MOVS, SCAS, STOS)的前缀.
    #rep指令的是重复后面的指令.ECX的值是重复的次数.

    #STOSL指令的作用是将eax中的值拷贝到ES:EDI指向的地址.

	cld;rep;stosl

	# 将页表pg0-pg3映射到页表目录_pg_dir中
    # 将这4个页表都设置为 可读可写&&任何特权级的程序都可以访问。
    # 页目录和页表中的每项信息4个字节, 其在内存中的位格式为
    # |31                     12|11   9               0
    # -------------------------------------------------
    # |                         |     |  | | |  |U|R| |
    # | page frame addr[31..12] | AVL |00|D|A|00|/|/|P|
    # |                         |     |  | | |  |S|W| |
    # -------------------------------------------------
    # 页表信息用于描述页表及其访问属性。
    # 页表地址(page table addr): 页表基址(以4k对齐, 低12位由CPU自动补0)。
    # AVL: 编程自定义使用。
    # D: 已修改位, 由硬件置位。
    # A: 已访问位, 由硬件置位。
    # R/W: 读/写位。1-可读可写; 0-仅可读或可执行。
    # U/S: 用户/超级用户位, 1-运行在任何特权级的程序皆可访问;0-超级用户特权级(0,1,2)程序可访问。
    # P: 页表是否可用; 1-可用;0-不可用。
    # 页框地址(PAGE FRAME ADDRESS)指定了一页内存的物理起始地址。因为内存页是位于4K
    # 边界上的，所以其低12 比特总是0，因此表项的低12 比特可作它用。
    # 在页目录中，表项的页框地址 是一个页表的起始的物理内存地址；
    # 在页表中，表项的页框地址，是内存操作的物理内存地址。
    # 例如〞$pg0+7"表示：0x00001007，是页目录表中的第1项。
	movl $pg0+7,pg_dir		/* set present bit/user r/w */
	movl $pg1+7,pg_dir+4		/*  --------- " " --------- */
	movl $pg2+7,pg_dir+8		/*  --------- " " --------- */
	movl $pg3+7,pg_dir+12		/*  --------- " " --------- */

	# 在pg0 - pg3 4个页表中设置页表项描述内存页。
    # 页表项用于描述一页内存, 其位格式同 页目录项。
    #
    # 将页表项设置为都有效,其对应的内存页都可读可写。
    #
    #
    # 页表项与内存页的对应关系为:
    # 页表    页表项偏移   内存页起始地址
    # pg3    4095        0x00fff007
    # pg3    4094        0x00ffe007
    #         ...            ...
    # pg0      1         0x00001007
    # pg0      0         0x00000007
	movl $pg3+4092,%edi    # edi 指向最后一页的最后一项。

	movl $0xfff007,%eax    # 16Mb - 4096 + 7 (r/w user, p)
	                       # 最后1 项对应物理内存页面的地址是 Oxfff000，
                           # 加上属性标志 7，即为 Oxfff007。
	std                    # 方向位置位，edi 值递减(4字节)。
1:	stosl			     /* fill pages backwards - more efficient :-) */
	subl $0x1000,%eax     # 每填写好一项，物理地址值减 0x1000。
	jge 1b
	cld

	# 1个页表4KB, 1个页表项4字节
    # 1个页表有1KB个页表项, 4个页表可设4KB个页表项,
    # 若每个页表项记录的一页内存大小4KB, 则一共可以记录16MB内存。
    #
    # 此处设置4个页表是为了映射操作系统内核所使用的16Mb内存,
    # 对于后续内存地址>16Mb内存的访问,操作系统内核程序会为其创建页表和页目录项来映射该段内存。
    #
    # 将页表目录_pg_dir地址(0x0)加载到CR3。
	xorl %eax,%eax		/* pg_dir is at 0x0000 */
	movl %eax,%cr3		/* cr3 - page directory start */

	# 设置CR0 PG位(bit[31])开启分页机制。
	movl %cr0,%eax
	orl $0x80000000,%eax
	movl %eax,%cr0		/* set paging (PG) bit */
	ret			/* this also flushes prefetch-queue */

.align 2  # 按4字节方式对齐内存地址边界。
.word 0   # 这里先空出2字节，这样 224 行上的长字是4字节对齐的。
# idt_descr处的6字节内容充当lidt指令操作数加载给IDTR,
# 低16位为IDT长度,  高32位为IDT地址(_idt)。
idt_descr:
	.word 256*8-1		# idt contains 256 entries
	.long idt

# gdt_descr处的6字节内容充当lgdt指令操作数加载给GDTR,
# 低16位为GDT长度, 高32位为GDT地址(_gdt)。
.align 2
.word 0
gdt_descr:
	.word 256*8-1		# so does gdt (not that that's any
	.long gdt		# magic number, but it works for me :^)

.align 8
# 中断向量表地址，8个字节一组，共 256组， 初始化为0
# IDT表空间。每个门描述符8字节，共占用2KB字节。
idt:	.fill 256,8,0		# idt is uninitialized
#
# 段描述符通用格式
# |31           24|23|22 |21|20 |19         16|15|14 13| 12|11   8|7                0|
# ------------------------------------------------------------------------------------
# | 基地址 31...24 |G |D/B|0 |AVL|段限长 19...16| P| DPL | S | TYPE |基地址 Base 23...16|
# ------------------------------------------------------------------------------------
# |          基地址 Base 15...0                |     段限长 Segment Limit 15...0       |
# ------------------------------------------------------------------------------------
# AVL 系统软件可用位
# LIMIT 段限长数值，需结合G来计算容量值
# BASE 段基地址
# P 段存在
# B/D 默认大小（0: 16 位;1: 32 位段）
# S 段描述符类型（0-系统；1-代码或数据）
# DPL 描述符特权级
# TYPE - 段类型，详见书94页
# G 颗粒度
#  该字段用于确定段限长字段 Limit 值的单位。
#  如果颗粒度标志为 0，则段限长值的单位是字节：
#  如果设置了颗粒度标志，则段限长值使用 4KB 单位。（这个标志不影响段基地址的颗粒度，基地
#  址的颗粒度总是字节单位。）若设置了 G标志，那么当使用段限长来检查偏移值时，并不会去检
#  查偏移值的12 位最低有效位。例如，当G-1时，段限长为1表明有效偏移值为0到 4095。
#
# GDT[0] 为保留的空描述符。
#
# GDT[1]描述内核代码段，地址是[0x0, 0x1000000), 16Mb
# 0x00c09a00 = 0000 0000 1100 0000 1001 1010 0000 0000
# G=1,D=1, 内存段颗粒度为4Kb, 默认操作数为32位。
# P=1(有效), S=1(代码或数据), TYPE=1010=1CRA (C=0,A=0,R=1(可读));
# 0xfff 表示段限长，4095个4KB，也就是16Mb
#
#
# GDT[2]描述内核数据段，地址是[0x0, 0x1000000), 16Mb,
# 0x00c09200 = 0000 0000 1100 0000 1001 0010 0000 0000
# G=1,D=1, 内存段颗粒度为4Kb, 默认操作数为32位。
# P=1(有效), S=1(代码或数据), TYPE=0010=0EWA (W=1(可写数据内存段), E=A=0;);
#
# GDT[3]保留。
# GDT[4..]供后续设置LDT或TSS描述符。
gdt:	.quad 0x0000000000000000	/* NULL descriptor */
	.quad 0x00c09a0000000fff	# 16Mb  第1个是内核代码段描述符。其选择符是0x08。
	.quad 0x00c0920000000fff	# 16Mb  第2个是内核数据段描述符。其选择符是0x10。
	.quad 0x0000000000000000	/* TEMPORARY - don't use */
	.fill 252,8,0			/* space for LDT's and TSS's etc */
